Skip to content

feat: add 'mobilecli device logs' support for streaming logs from mobile devices#195

Open
gmegidish wants to merge 13 commits intomainfrom
feat/device-logs
Open

feat: add 'mobilecli device logs' support for streaming logs from mobile devices#195
gmegidish wants to merge 13 commits intomainfrom
feat/device-logs

Conversation

@gmegidish
Copy link
Copy Markdown
Member

No description provided.

Adds real-time log streaming from iOS simulators (via xcrun simctl log stream)
and real iOS devices (via go-ios syslog_relay). Android and remote devices
return "not yet supported". Supports --limit flag to stop after N entries.
Filters are applied before --limit, so the user sees exactly N matching
entries. --process does substring matching on the process name.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a device logs CLI subcommand with --limit, repeatable --filter, and --device flags. Introduces LogEntry, extends ControllableDevice with StreamLogs(onLog func(LogEntry) bool) error, and implements StreamLogs for iOS, Simulator, and Android; RemoteDevice.StreamLogs returns unsupported. Adds ParseLogFilters and LogsCommand to apply allowlisted key=value/key!=value filters and stream matching entries as JSON Lines, stopping on optional limit or caller stop. Updates CLI help/README and adjusts go.mod dependency versions.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the changes, implementation approach, and any testing performed for the device logs feature.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding device logs streaming functionality to the mobilecli CLI tool.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/device-logs

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
commands/logs.go (1)

31-33: PID filter condition may have unintended behavior with PID 0.

The condition req.PID >= 0 means PID 0 is treated as a valid filter value. If a user passes --pid 0, it would filter for processes with PID 0 (typically the kernel scheduler). This might be intentional, but consider whether -1 should be the only "unset" sentinel, or if you want to use a pointer type (*int) to distinguish "not set" from "filter for PID 0".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/logs.go` around lines 31 - 33, The PID-check treats 0 as a valid
filter because it uses req.PID >= 0; change the filter to distinguish "unset"
from a real PID by making req.PID a pointer (*int) or by using -1 as the
explicit unset sentinel; then update the check in the logs filtering code to
something like "if req.PID != nil && entry.PID != *req.PID { return true }" (or
if using sentinel: "if req.PID != -1 && entry.PID != req.PID { return true }")
and adjust flag/constructors that populate req.PID accordingly so callers can
actually signal "no PID filter".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/logs.go`:
- Around line 35-37: The current handler silently swallows JSON encoding
failures by returning false on encoder.Encode(entry) and later returning
NewSuccessResponse("done"); modify the error path so encode failures are
propagated instead of masked: when encoder.Encode(entry) returns an error,
return a failure value (e.g., propagate the error to the caller or return a
NewErrorResponse with err.Error()) rather than returning false, and ensure the
function no longer returns NewSuccessResponse("done") when an encode error
occurred. Update the code around encoder.Encode(entry) to surface the actual err
and stop further processing, and adjust any callers of this function to handle
the propagated error if needed.

In `@devices/ios.go`:
- Around line 1667-1717: StreamLogs currently calls d.getEnhancedDevice()
directly which can fail on iOS 17+ if the tunnel isn't started; mirror other
methods (Reboot, LaunchApp, TerminateApp) by calling d.startTunnel() first and
returning any error. Modify StreamLogs to invoke d.startTunnel(), check and
return its error, then proceed to call d.getEnhancedDevice() and the rest of the
existing logic; ensure you reference the IOSDevice methods startTunnel and
getEnhancedDevice and preserve existing defer conn.Close() behavior.

---

Nitpick comments:
In `@commands/logs.go`:
- Around line 31-33: The PID-check treats 0 as a valid filter because it uses
req.PID >= 0; change the filter to distinguish "unset" from a real PID by making
req.PID a pointer (*int) or by using -1 as the explicit unset sentinel; then
update the check in the logs filtering code to something like "if req.PID != nil
&& entry.PID != *req.PID { return true }" (or if using sentinel: "if req.PID !=
-1 && entry.PID != req.PID { return true }") and adjust flag/constructors that
populate req.PID accordingly so callers can actually signal "no PID filter".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c7acb32-e5d2-4071-b9d8-7d12c6621eaf

📥 Commits

Reviewing files that changed from the base of the PR and between d87b358 and c5fdce5.

📒 Files selected for processing (7)
  • cli/logs.go
  • commands/logs.go
  • devices/android.go
  • devices/common.go
  • devices/ios.go
  • devices/remote.go
  • devices/simulator.go

Comment thread commands/logs.go
Comment thread devices/ios.go Outdated
Streams logs via adb logcat with threadtime,year format. Parses using
existing logcatLineRegex. Resolves PID to package name via one-time
adb shell ps call for --process filtering. Adds tag field to LogEntry
(populated for Android, omitted for iOS).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
commands/logs.go (1)

34-36: ⚠️ Potential issue | 🟠 Major

Propagate JSON encode failures instead of returning success.

At Line 34, encode errors stop the callback, but the error is discarded; then Line 47 can still return success. This masks partial stream failures.

Suggested fix
 	encoder := json.NewEncoder(os.Stdout)
 	count := 0
+	var encodeErr error
 	err = device.StreamLogs(func(entry devices.LogEntry) bool {
 		if req.Process != "" && entry.Process != req.Process {
 			return true
 		}
 		if req.PID >= 0 && entry.PID != req.PID {
 			return true
 		}

 		if err := encoder.Encode(entry); err != nil {
+			encodeErr = err
 			return false
 		}
 		count++
 		if req.Limit > 0 && count >= req.Limit {
 			return false
 		}
 		return true
 	})
 	if err != nil {
 		return NewErrorResponse(fmt.Errorf("error streaming logs: %w", err))
 	}
+	if encodeErr != nil {
+		return NewErrorResponse(fmt.Errorf("error encoding log entry: %w", encodeErr))
+	}

 	return NewSuccessResponse("done")

Also applies to: 43-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/logs.go` around lines 34 - 36, The JSON encode failures are being
swallowed where encoder.Encode(entry) is called; change the callback to
propagate the error instead of returning a bool success sentinel: when
encoder.Encode(entry) (and the similar block at 43-47) returns an error, return
that error (or set a named err variable and break) so the caller can detect and
surface the failure, and update the outer caller/return path to return the error
rather than always returning success; reference the encoder.Encode(entry) calls
and the surrounding callback/handler to locate and update the code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@commands/logs.go`:
- Around line 34-36: The JSON encode failures are being swallowed where
encoder.Encode(entry) is called; change the callback to propagate the error
instead of returning a bool success sentinel: when encoder.Encode(entry) (and
the similar block at 43-47) returns an error, return that error (or set a named
err variable and break) so the caller can detect and surface the failure, and
update the outer caller/return path to return the error rather than always
returning success; reference the encoder.Encode(entry) calls and the surrounding
callback/handler to locate and update the code paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcf44939-e9f5-4879-b35b-2ccb292cf83b

📥 Commits

Reviewing files that changed from the base of the PR and between c5fdce5 and 96c026c.

📒 Files selected for processing (3)
  • commands/logs.go
  • devices/android.go
  • devices/common.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • devices/common.go
  • devices/android.go

- Fix --process flag description to say "exact match"
- Split callback into matchesFilter and emit helpers
- Extract processNameFromPath and atoiOrZero to common.go
Supports key=value (include) and key!=value (exclude) syntax.
Multiple filters are ANDed. Valid keys: pid, process, tag, level,
subsystem, category, message. Invalid keys return a clear error.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devices/simulator.go`:
- Around line 1072-1116: The code currently treats any json.Decoder errors as
normal end-of-stream and kills the child without reaping it; update the
log-stream handling so that failures from decoder.Token() or decoder.Decode()
that are not io.EOF are returned as errors and the child is reaped: after
calling cmd.Process.Kill() invoke cmd.Wait() and include its error in the
returned fmt.Errorf; when decoder.Decode() returns io.EOF treat it as normal
end-of-stream, but for other errors return them (again ensuring cmd.Wait() is
called and its error combined); finally, don't ignore the result of cmd.Wait()
at the end—return any non-nil wait error. Add the io import and reference
functions/values like cmd.Start, decoder.Token, decoder.Decode,
cmd.Process.Kill, cmd.Wait, simctlLogEntry, and onLog when making these fixes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d003d51-ab06-4dfe-8267-12fe09f41d2e

📥 Commits

Reviewing files that changed from the base of the PR and between 96c026c and 561a210.

📒 Files selected for processing (6)
  • README.md
  • cli/logs.go
  • commands/logs.go
  • devices/common.go
  • devices/ios.go
  • devices/simulator.go
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/logs.go

Comment thread devices/simulator.go
Replaces legacy syslog_relay (ASL) with structured os_trace_relay via
go-ios v1.0.211's new ostrace package. Real iOS devices now expose the
same subsystem/category/structured-level fields the simulator already
provided, so cross-platform --filter keys work uniformly on hardware.

Removes EventType from LogEntry: it was simctl-only metadata that could
not be populated on iOS-real (the ostrace binding does not surface
record types and we filter to log messages only) or Android (logcat
has no equivalent), and no consumer read it. Drops the now-unused
atoiOrZero helper.

Documents device logs --filter in the root help text.
iOS real device: StreamLogs now starts the iOS 17+ tunnel before opening
the os_trace_relay connection, matching Reboot/LaunchApp/TerminateApp.
Without this, ostrace.New fails on devices that need a tunnel.

Simulator: surface real read failures from the simctl JSON stream and
reap the child process on every error path. Previously, decoder.Token
and decoder.Decode errors were swallowed as "stream ended" and
cmd.Wait was ignored, so a non-zero simctl exit (bad UDID, device
shutdown mid-stream) returned success and left a zombie. EOF from
Decode is still treated as a normal end-of-stream; caller-initiated
stops are tracked separately so the kill we issue ourselves is not
reported as an error.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devices/android.go`:
- Around line 1408-1450: The PID→process map is created once by calling
getPidToProcessMap() before starting adb logcat, so processes started after the
stream begins never get resolved; update the code inside the scanner loop to
refresh or requery the PID map when needed (e.g., call getPidToProcessMap()
again on a cache miss or at a regular interval) before assigning entry.Process,
referencing pidMap, getPidToProcessMap(), the scanner loop that iterates
parseLogcatLine(line), and the LogEntry.Process assignment so new processes
launched after log streaming starts will be correctly resolved for --process
filtering.
- Around line 1424-1459: StreamLogs currently swallows scanner and subprocess
errors; change it to propagate them: after the scanner loop check scanner.Err()
and return it if non-nil (unless we intentionally stopped because onLog returned
false), and after breaking/killing the child call cmd.Wait() and return its
error instead of always returning nil; use the onLog return value to distinguish
an intentional stop (treat as nil) from actual errors, and ensure any error from
cmd.Wait() is returned when the stop was not intentional. Reference: StreamLogs
function, scanner variable, onLog callback, and cmd.Wait()/cmd.Process.Kill()
calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8bdd6cd-3f76-49de-9adb-4b9465c7d845

📥 Commits

Reviewing files that changed from the base of the PR and between 561a210 and c9fc86a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • README.md
  • cli/root.go
  • devices/android.go
  • devices/common.go
  • devices/ios.go
  • devices/remote.go
  • devices/simulator.go
  • go.mod
✅ Files skipped from review due to trivial changes (2)
  • cli/root.go
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • devices/remote.go

Comment thread devices/android.go Outdated
Comment thread devices/android.go
Comment on lines +1424 to +1459
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()

parsed := parseLogcatLine(line)
if parsed == nil {
continue
}

pid, _ := strconv.Atoi(parsed.PID)
level := logcatLevelMap[parsed.Level]
if level == "" {
level = parsed.Level
}

entry := LogEntry{
Timestamp: parsed.Date + " " + parsed.Time,
PID: pid,
Level: level,
Tag: parsed.Tag,
Message: parsed.Message,
}

// resolve process name from ps map (for --process filtering)
if pidMap != nil {
entry.Process = pidMap[pid]
}

if !onLog(entry) {
_ = cmd.Process.Kill()
break
}
}

_ = cmd.Wait()
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the file and examine the target lines
git ls-files devices/android.go

Repository: mobile-next/mobilecli

Length of output: 84


🏁 Script executed:

# Get the file size to ensure we can read it
wc -l devices/android.go

Repository: mobile-next/mobilecli

Length of output: 89


🏁 Script executed:

# Read the target lines and context around them
sed -n '1400,1470p' devices/android.go

Repository: mobile-next/mobilecli

Length of output: 1360


Propagate scanner and subprocess errors instead of silently returning success.

The StreamLogs function declares error as its return type but always returns nil, discarding both scanner.Err() and cmd.Wait() results. Device disconnects, adb failures, broken pipes, and scanner read errors are invisible to callers. The distinction between intentional stop (when onLog returns false) and actual errors must be preserved.

Suggested fix
 	scanner := bufio.NewScanner(stdout)
+	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
+	stoppedByCaller := false
 	for scanner.Scan() {
 		line := scanner.Text()
@@
 		if !onLog(entry) {
+			stoppedByCaller = true
 			_ = cmd.Process.Kill()
 			break
 		}
 	}
 
-	_ = cmd.Wait()
-	return nil
+	if err := scanner.Err(); err != nil && !stoppedByCaller {
+		_ = cmd.Process.Kill()
+		_ = cmd.Wait()
+		return fmt.Errorf("failed reading logcat output: %w", err)
+	}
+
+	if err := cmd.Wait(); err != nil && !stoppedByCaller {
+		return fmt.Errorf("logcat exited unexpectedly: %w", err)
+	}
+	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
parsed := parseLogcatLine(line)
if parsed == nil {
continue
}
pid, _ := strconv.Atoi(parsed.PID)
level := logcatLevelMap[parsed.Level]
if level == "" {
level = parsed.Level
}
entry := LogEntry{
Timestamp: parsed.Date + " " + parsed.Time,
PID: pid,
Level: level,
Tag: parsed.Tag,
Message: parsed.Message,
}
// resolve process name from ps map (for --process filtering)
if pidMap != nil {
entry.Process = pidMap[pid]
}
if !onLog(entry) {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
stoppedByCaller := false
for scanner.Scan() {
line := scanner.Text()
parsed := parseLogcatLine(line)
if parsed == nil {
continue
}
pid, _ := strconv.Atoi(parsed.PID)
level := logcatLevelMap[parsed.Level]
if level == "" {
level = parsed.Level
}
entry := LogEntry{
Timestamp: parsed.Date + " " + parsed.Time,
PID: pid,
Level: level,
Tag: parsed.Tag,
Message: parsed.Message,
}
// resolve process name from ps map (for --process filtering)
if pidMap != nil {
entry.Process = pidMap[pid]
}
if !onLog(entry) {
stoppedByCaller = true
_ = cmd.Process.Kill()
break
}
}
if err := scanner.Err(); err != nil && !stoppedByCaller {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return fmt.Errorf("failed reading logcat output: %w", err)
}
if err := cmd.Wait(); err != nil && !stoppedByCaller {
return fmt.Errorf("logcat exited unexpectedly: %w", err)
}
return nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devices/android.go` around lines 1424 - 1459, StreamLogs currently swallows
scanner and subprocess errors; change it to propagate them: after the scanner
loop check scanner.Err() and return it if non-nil (unless we intentionally
stopped because onLog returned false), and after breaking/killing the child call
cmd.Wait() and return its error instead of always returning nil; use the onLog
return value to distinguish an intentional stop (treat as nil) from actual
errors, and ensure any error from cmd.Wait() is returned when the stop was not
intentional. Reference: StreamLogs function, scanner variable, onLog callback,
and cmd.Wait()/cmd.Process.Kill() calls.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
devices/simulator.go (1)

925-932: Optional: deduplicate process termination/wait error plumbing.

Line 925, Line 930, and Line 943 repeat the same kill+wait pattern. A small local helper would reduce repetition and keep future error-path changes safer.

Refactor sketch
 func (s *SimulatorDevice) StreamLogs(onLog func(LogEntry) bool) error {
@@
-	// read opening '[' of the JSON array
+	terminateWithDecodeErr := func(prefix string, cause error) error {
+		_ = cmd.Process.Kill()
+		waitErr := cmd.Wait()
+		if waitErr != nil {
+			return fmt.Errorf("%s: %w (process exit: %v)", prefix, cause, waitErr)
+		}
+		return fmt.Errorf("%s: %w", prefix, cause)
+	}
+
+	// read opening '[' of the JSON array
 	token, err := decoder.Token()
 	if err != nil {
-		_ = cmd.Process.Kill()
-		waitErr := cmd.Wait()
-		return fmt.Errorf("failed to read opening token: %w (process exit: %v)", err, waitErr)
+		return terminateWithDecodeErr("failed to read opening token", err)
 	}
 	if delim, ok := token.(json.Delim); !ok || delim != '[' {
-		_ = cmd.Process.Kill()
-		waitErr := cmd.Wait()
-		return fmt.Errorf("expected '[', got %v (process exit: %v)", token, waitErr)
+		return terminateWithDecodeErr(fmt.Sprintf("expected '[', got %v", token), errors.New("invalid opening token"))
 	}
@@
 		if err := decoder.Decode(&raw); err != nil {
 			if errors.Is(err, io.EOF) {
 				break
 			}
-			_ = cmd.Process.Kill()
-			waitErr := cmd.Wait()
-			return fmt.Errorf("failed to decode log entry: %w (process exit: %v)", err, waitErr)
+			return terminateWithDecodeErr("failed to decode log entry", err)
 		}

Also applies to: 943-945, 959-966

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devices/simulator.go` around lines 925 - 932, The error paths repeatedly call
cmd.Process.Kill() followed by cmd.Wait() and propagate wait errors (e.g., the
blocks handling token errors around the token/json.Delim checks), so extract
that plumbing into a small helper (for example killAndWait or terminateCmd) that
accepts the *exec.Cmd and returns the wait error (or combined error), then
replace each duplicated pair (the occurrences using cmd.Process.Kill() and
cmd.Wait() in the token read/expect blocks) with a single call to that helper
and use its returned error in the fmt.Errorf calls to keep semantics identical
while removing duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devices/ios.go`:
- Around line 1632-1673: The StreamLogs method calls the callback onLog without
checking for nil which can panic; add a nil guard around the invocation in
IOSDevice.StreamLogs so callers may pass nil safely. Specifically, in function
StreamLogs (method on type IOSDevice) replace the unconditional call to
onLog(out) with a check like: if onLog != nil { if !onLog(out) { return nil } }
so that when onLog is nil the log loop continues/terminates normally without
invoking a nil function pointer.

---

Nitpick comments:
In `@devices/simulator.go`:
- Around line 925-932: The error paths repeatedly call cmd.Process.Kill()
followed by cmd.Wait() and propagate wait errors (e.g., the blocks handling
token errors around the token/json.Delim checks), so extract that plumbing into
a small helper (for example killAndWait or terminateCmd) that accepts the
*exec.Cmd and returns the wait error (or combined error), then replace each
duplicated pair (the occurrences using cmd.Process.Kill() and cmd.Wait() in the
token read/expect blocks) with a single call to that helper and use its returned
error in the fmt.Errorf calls to keep semantics identical while removing
duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d0fedcd-9eba-4938-b8a2-ac8010ee693d

📥 Commits

Reviewing files that changed from the base of the PR and between c9fc86a and b53ca63.

📒 Files selected for processing (2)
  • devices/ios.go
  • devices/simulator.go

Comment thread devices/ios.go Outdated
Comment on lines +1632 to +1673
func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error {
// ensure tunnel is running for iOS 17+
err := d.startTunnel()
if err != nil {
return fmt.Errorf("failed to start tunnel: %w", err)
}

device, err := d.getEnhancedDevice()
if err != nil {
return fmt.Errorf("failed to get device: %w", err)
}

spec := ostrace.DefaultLevelFilter()
conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags)
if err != nil {
return fmt.Errorf("failed to connect to os_trace_relay: %w", err)
}
defer func() { _ = conn.Close() }()

for {
entry, err := conn.ReadEntry()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("os_trace read error: %w", err)
}

out := LogEntry{
Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"),
Message: entry.Message,
Level: entry.LevelName,
PID: int(entry.PID),
Process: processNameFromPath(entry.Filename),
}
if entry.Label != nil {
out.Subsystem = entry.Label.Subsystem
out.Category = entry.Label.Category
}

if !onLog(out) {
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a nil guard for onLog to prevent panic.

At Line 1672, onLog is called unconditionally. If a caller passes nil, this will panic and terminate the command path.

Proposed fix
 func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error {
+	if onLog == nil {
+		return fmt.Errorf("onLog callback cannot be nil")
+	}
+
 	// ensure tunnel is running for iOS 17+
 	err := d.startTunnel()
 	if err != nil {
 		return fmt.Errorf("failed to start tunnel: %w", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error {
// ensure tunnel is running for iOS 17+
err := d.startTunnel()
if err != nil {
return fmt.Errorf("failed to start tunnel: %w", err)
}
device, err := d.getEnhancedDevice()
if err != nil {
return fmt.Errorf("failed to get device: %w", err)
}
spec := ostrace.DefaultLevelFilter()
conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags)
if err != nil {
return fmt.Errorf("failed to connect to os_trace_relay: %w", err)
}
defer func() { _ = conn.Close() }()
for {
entry, err := conn.ReadEntry()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("os_trace read error: %w", err)
}
out := LogEntry{
Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"),
Message: entry.Message,
Level: entry.LevelName,
PID: int(entry.PID),
Process: processNameFromPath(entry.Filename),
}
if entry.Label != nil {
out.Subsystem = entry.Label.Subsystem
out.Category = entry.Label.Category
}
if !onLog(out) {
return nil
func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error {
if onLog == nil {
return fmt.Errorf("onLog callback cannot be nil")
}
// ensure tunnel is running for iOS 17+
err := d.startTunnel()
if err != nil {
return fmt.Errorf("failed to start tunnel: %w", err)
}
device, err := d.getEnhancedDevice()
if err != nil {
return fmt.Errorf("failed to get device: %w", err)
}
spec := ostrace.DefaultLevelFilter()
conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags)
if err != nil {
return fmt.Errorf("failed to connect to os_trace_relay: %w", err)
}
defer func() { _ = conn.Close() }()
for {
entry, err := conn.ReadEntry()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("os_trace read error: %w", err)
}
out := LogEntry{
Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"),
Message: entry.Message,
Level: entry.LevelName,
PID: int(entry.PID),
Process: processNameFromPath(entry.Filename),
}
if entry.Label != nil {
out.Subsystem = entry.Label.Subsystem
out.Category = entry.Label.Category
}
if !onLog(out) {
return nil
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devices/ios.go` around lines 1632 - 1673, The StreamLogs method calls the
callback onLog without checking for nil which can panic; add a nil guard around
the invocation in IOSDevice.StreamLogs so callers may pass nil safely.
Specifically, in function StreamLogs (method on type IOSDevice) replace the
unconditional call to onLog(out) with a check like: if onLog != nil { if
!onLog(out) { return nil } } so that when onLog is nil the log loop
continues/terminates normally without invoking a nil function pointer.

…onnections

- thread context.Context through ControllableDevice.StreamLogs interface
- use exec.CommandContext on Android and Simulator so the subprocess is killed automatically when the context is cancelled
- replace blocking iOS ReadEntry loop with goroutine+channel+select to support ctx.Done without hanging on dead connections
- wire signal.NotifyContext in CLI so Ctrl+C cancels the context cleanly
- fix Android scanner: larger 256KB buffer and check scanner.Err() after loop
…roid

resolve process names on demand via /proc/<pid>/cmdline instead of a
one-shot ps snapshot at startup, so long-running streams stay accurate
as processes come and go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant